// Copyright (C) 2012 jOVAL.org. All rights reserved. // This software is licensed under the AGPL 3.0 license available at http://www.joval.org/agpl_v3.txt package jwsmv.wsman; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.PasswordAuthentication; import java.net.URL; import java.math.BigInteger; import java.security.PrivilegedActionException; import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Properties; import java.util.UUID; import javax.security.auth.login.LoginException; import javax.security.auth.login.FailedLoginException; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.transform.dom.DOMSource; import javax.xml.ws.http.HTTPException; import org.w3c.dom.Node; import org.slf4j.cal10n.LocLogger; import org.dmtf.wsman.Locale; import org.xmlsoap.ws.addressing.AttributedURI; import org.xmlsoap.ws.addressing.EndpointReferenceType; import org.xmlsoap.ws.transfer.AnyXmlOptionalType; import org.xmlsoap.ws.transfer.AnyXmlType; import org.w3c.soap.envelope.Body; import org.w3c.soap.envelope.Envelope; import org.w3c.soap.envelope.Fault; import org.w3c.soap.envelope.Header; import jwsmv.Constants; import jwsmv.Message; import jwsmv.http.HttpSocketConnection; import jwsmv.http.NtlmHttpURLConnection; import jwsmv.util.Base64; /** * A Web-Services port implementation for MS-WSMV (Microsoft Web Services Management for Vista). * * Since MS-WSMV is implemented on top of WS-Management, including the non-BP/1.0 compliant WS-Transfer specification, * it is necessary to implement a custom SOAP client to perform the various operations therein entailed. * * @author David A. Solin * @version %I% %G% */ public class Port implements Constants { private static final String RESOURCE = "ws-man.properties"; private static final JAXBContext JAXB; static { ClassLoader cl = Port.class.getClassLoader(); Properties props = new Properties(); InputStream rsc = cl.getResourceAsStream(RESOURCE); if (rsc == null) { throw new RuntimeException(Message.getMessage(Message.ERROR_MISSING_RESOURCE, RESOURCE)); } else { try { props.load(rsc); JAXB = JAXBContext.newInstance(props.getProperty("ws-man.packages"), cl); } catch (Exception e) { throw new RuntimeException(e); } } } public static final Marshaller getMarshaller() throws JAXBException { return JAXB.createMarshaller(); } public static final Unmarshaller getUnmarshaller() throws JAXBException { return JAXB.createUnmarshaller(); } /** * Enumeration of supported authentication schemes. */ public enum AuthScheme { NONE, BASIC, NTLM; } private AuthScheme scheme; private String url; private Proxy proxy; private PasswordAuthentication cred, proxyCred = null; private boolean encrypt; private OutputStream debug; private LocLogger logger; /** * Create a SOAP Web-Services port. */ public Port(String url, PasswordAuthentication cred) throws JAXBException { scheme = AuthScheme.NTLM; logger = Message.getLogger(); proxy = Proxy.NO_PROXY; this.url = url; this.cred = cred; this.encrypt = true; this.debug = null; } /** * Set a proxy and proxy credentials. */ public void setProxy(Proxy proxy, PasswordAuthentication proxyCred) { this.proxy = proxy; this.proxyCred = proxyCred; } /** * Enable/disable SOAP encryption. */ public void setEncryption(boolean encrypt) { this.encrypt = encrypt; } /** * Enable debug logging by setting an OutputStream to which to log SOAP traffic (or null to disable). */ public void setDebug(OutputStream debug) { this.debug = debug; } public void setLogger(LocLogger logger) { this.logger = logger; } public LocLogger getLogger() { return logger; } public Object dispatch(String action, List<Object> headers, Object input) throws IOException, HTTPException, JAXBException, FaultException, FailedLoginException { Unmarshaller unmarshaller = JAXB.createUnmarshaller(); Marshaller marshaller = JAXB.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); Header header = Factories.SOAP.createHeader(); AttributedURI to = Factories.ADDRESS.createAttributedURI(); to.setValue(url); to.getOtherAttributes().put(MUST_UNDERSTAND, "true"); header.getAny().add(Factories.ADDRESS.createTo(to)); EndpointReferenceType endpointRef = Factories.ADDRESS.createEndpointReferenceType(); AttributedURI address = Factories.ADDRESS.createAttributedURI(); address.setValue(REPLY_TO); address.getOtherAttributes().put(MUST_UNDERSTAND, "true"); endpointRef.setAddress(address); header.getAny().add(Factories.ADDRESS.createReplyTo(endpointRef)); AttributedURI soapAction = Factories.ADDRESS.createAttributedURI(); soapAction.setValue(action); soapAction.getOtherAttributes().put(MUST_UNDERSTAND, "true"); header.getAny().add(Factories.ADDRESS.createAction(soapAction)); AttributedURI messageId = Factories.ADDRESS.createAttributedURI(); messageId.setValue("uuid:" + UUID.randomUUID().toString().toUpperCase()); header.getAny().add(Factories.ADDRESS.createMessageID(messageId)); for (Object obj : headers) { header.getAny().add(obj); } Locale locale = Factories.WSMAN.createLocale(); locale.setLang("en-US"); locale.getOtherAttributes().put(MUST_UNDERSTAND, "false"); header.getAny().add(locale); // // Convert any non-directly-serializable WS-Transfer input types // if (input instanceof AnyXmlType) { input = ((AnyXmlType)input).getAny(); } else if (input instanceof AnyXmlOptionalType) { input = ((AnyXmlOptionalType)input).getAny(); } Body body = Factories.SOAP.createBody(); if (input != null) { body.getAny().add(input); } Envelope request = Factories.SOAP.createEnvelope(); request.setHeader(header); request.setBody(body); URL u = new URL(url); boolean retry = false; Object result = null; HttpURLConnection conn = null; do { try { if (conn != null) { conn.disconnect(); } logger.trace(Message.STATUS_CONNECT, url, scheme); switch(scheme) { case NONE: switch(proxy.type()) { case DIRECT: conn = (HttpURLConnection)u.openConnection(); break; default: conn = (HttpURLConnection)u.openConnection(proxy); break; } break; case NTLM: conn = NtlmHttpURLConnection.openConnection(u, cred, encrypt); ((NtlmHttpURLConnection)conn).setProxy(proxy, proxyCred); break; case BASIC: switch(proxy.type()) { case DIRECT: conn = (HttpURLConnection)u.openConnection(); break; default: conn = (HttpURLConnection)u.openConnection(proxy); if (proxyCred != null) { String clear = proxyCred.getUserName() + ":" + new String(proxyCred.getPassword()); String auth = "Basic " + Base64.encodeBytes(clear.getBytes()); conn.setRequestProperty("Proxy-Authorization", auth); } break; } break; } conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/soap+xml;charset=UTF-8"); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); marshaller.marshal(Factories.SOAP.createEnvelope(request), buffer); byte[] bytes = buffer.toByteArray(); conn.setFixedLengthStreamingMode(bytes.length); conn.connect(); OutputStream out = conn.getOutputStream(); out.write(bytes); out.flush(); logger.debug(Message.STATUS_REQUEST, action); if (debug != null) { StringBuffer sb = new StringBuffer("[").append(new Date().toString()).append("] - SOAP Request:\r\n"); debug.write(sb.toString().getBytes()); debug.write(bytes); debug.write("\r\n".getBytes()); debug.flush(); } retry = false; int code = conn.getResponseCode(); switch(code) { case HttpURLConnection.HTTP_INTERNAL_ERROR: result = getSOAPBodyContents(unmarshaller, marshaller, conn.getErrorStream(), conn.getContentType()); break; case HttpURLConnection.HTTP_OK: result = getSOAPBodyContents(unmarshaller, marshaller, conn.getInputStream(), conn.getContentType()); break; case HttpURLConnection.HTTP_UNAUTHORIZED: retry = true; break; default: logger.warn(Message.ERROR_RESPONSE, code); debug(conn); throw new HTTPException(code); } } finally { if (conn != null) { conn.disconnect(); } } } while (retry && nextAuthScheme(conn)); if (result instanceof JAXBElement) { result = ((JAXBElement)result).getValue(); } logger.debug(Message.STATUS_RESPONSE, result == null ? "null" : result.getClass().getName()); if (result instanceof Fault) { throw new FaultException((Fault)result); } else { return result; } } // Private /** * Read a SOAP envelope and return the unmarshalled object contents of the body. */ private Object getSOAPBodyContents(Unmarshaller unmarshaller, Marshaller marshaller, InputStream in, String contentType) throws JAXBException, IOException { Object result = unmarshaller.unmarshal(in); in.close(); if (debug != null) { StringBuffer sb = new StringBuffer("[").append(new Date().toString()).append("] - SOAP Reply:\r\n"); debug.write(sb.toString().getBytes()); marshaller.marshal(result, debug); debug.write("\r\n".getBytes()); debug.flush(); } if (result instanceof JAXBElement) { JAXBElement elt = (JAXBElement)result; if (elt.getValue() instanceof Envelope) { List<Object> list = ((Envelope)elt.getValue()).getBody().getAny(); switch(list.size()) { case 0: return null; case 1: return list.get(0); default: return list; } } else { System.out.println("Unsupported element contents: " + elt.getValue().getClass().getName()); } } else { System.out.println("Unsupported class: " + result.getClass().getName()); } return null; } /** * Write information about a non-HTTP_OK reply to the debug log, if applicable. */ private void debug(HttpURLConnection conn) throws IOException { if (debug != null) { StringBuffer sb = new StringBuffer("[").append(new Date().toString()).append("] - HTTP Reply:\r\n"); debug.write(sb.toString().getBytes()); int len = conn.getHeaderFields().size(); if (len > 0) { sb = new StringBuffer(" ").append(conn.getHeaderField(0)).append("\r\n"); debug.write(sb.toString().getBytes()); for (int i=1; i < len; i++) { sb = new StringBuffer(" "); sb.append(conn.getHeaderFieldKey(i)).append(": ").append(conn.getHeaderField(i)).append("\n"); debug.write(sb.toString().getBytes()); } } len = conn.getContentLength(); if (len > 0) { byte[] buff = new byte[len]; for (int offset=0; offset < len; ) { offset += conn.getErrorStream().read(buff, offset, len - offset); } debug.write(buff); debug.write("\r\n".getBytes()); } debug.flush(); } } /** * Set the authentication scheme for the retry, or return false if there are no more options. * * @throws FailedLoginException if the specified login scheme was already attempted. */ private boolean nextAuthScheme(HttpURLConnection conn) throws FailedLoginException { if (conn == null) { return false; } List<String> authFields = conn.getHeaderFields().get("WWW-Authenticate"); if (authFields == null || authFields.size() == 0) { switch(scheme) { case NONE: throw new FailedLoginException(); default: scheme = AuthScheme.NONE; return true; } } boolean basic = false; boolean ntlm = false; boolean negotiate = false; for (String val : authFields) { if (val.toLowerCase().startsWith("basic")) { basic = true; } else if (val.equalsIgnoreCase("Negotiate")) { negotiate = true; } else if (val.equalsIgnoreCase("NTLM")) { ntlm = true; } } if (basic) { switch(scheme) { case BASIC: throw new FailedLoginException(); default: scheme = AuthScheme.BASIC; return true; } } if (negotiate || ntlm) { switch(scheme) { case NONE: case BASIC: scheme = AuthScheme.NTLM; return true; case NTLM: throw new FailedLoginException(); } } return false; } }